﻿//  Copyright © 2009-2010 by Rhy A. Mednick
//  All rights reserved.
//  http://rhyduino.codeplex.com
//  
//  Redistribution and use in source and binary forms, with or without modification, 
//  are permitted provided that the following conditions are met:
//  
//  * Redistributions of source code must retain the above copyright notice, this list 
//    of conditions and the following disclaimer.
//  
//  * Redistributions in binary form must reproduce the above copyright notice, this 
//    list of conditions and the following disclaimer in the documentation and/or other 
//    materials provided with the distribution.
//  
//  * Neither the name of Rhy A. Mednick nor the names of its contributors may be used 
//    to endorse or promote products derived from this software without specific prior 
//    written permission.
//  
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
//  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
//  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
//  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
//  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
//  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
//  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
//  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
//  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
//  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
//  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;

namespace TracerX
{
    /// <summary>
    ///   Watches for changes to a file by combining FileSystemWatcher with polling.
    /// </summary>
    internal class FileWatcher : IDisposable
    {
        private const int _maxInterval = 4000;
        private readonly FileInfo _fileInfo;
        private readonly FileSystemWatcher _fsw;
        private readonly object _lock = new object();
        private readonly Timer _pollTimer;
        private long _lastSize;
        private DateTime _lastWriteTime;
        private int _pollInterval = 500;

        public FileWatcher(string filepath)
        {
            _fsw = new FileSystemWatcher(Path.GetDirectoryName(filepath), Path.GetFileName(filepath));
            _fsw.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime | NotifyFilters.DirectoryName |
                                NotifyFilters.FileName | NotifyFilters.Size;
            _fsw.IncludeSubdirectories = false;

            _fsw.Changed += new FileSystemEventHandler(_fsw_Changed);
            _fsw.Created += _fsw_Changed;
            _fsw.Deleted += _fsw_Changed;
            _fsw.Renamed += new RenamedEventHandler(_fsw_Renamed);
            _fsw.Error += new ErrorEventHandler(_fsw_Error);

            _fsw.EnableRaisingEvents = true;

            _fileInfo = new FileInfo(filepath);
            _lastWriteTime = _fileInfo.LastWriteTime;
            _lastSize = _fileInfo.Length;
            _pollTimer = new Timer(new TimerCallback(PollTimer), null, _pollInterval, _pollInterval);
        }

        public bool Stopped { get; private set; }

        #region IDisposable Members

        public void Dispose()
        {
            Stop();
        }

        #endregion

        public event FileSystemEventHandler Changed;
        public event RenamedEventHandler Renamed;

        private void Stop()
        {
            lock (_lock)
            {
                Stopped = true;
                Debug.Print("Stopping file watcher.");
                _fsw.Dispose();
                _pollTimer.Dispose();
            }
        }

        private void ResetPollInterval()
        {
            if (Stopped) return;

            _pollTimer.Change(500, 500);
            _pollInterval = 500;
        }

        private void NextInterval()
        {
            if (Stopped) return;

            if (_pollInterval < _maxInterval)
            {
                _pollInterval = Math.Min(_maxInterval, _pollInterval*2);
                _pollTimer.Change(_pollInterval, _pollInterval);
            }
        }

        private void _fsw_Renamed(object sender, RenamedEventArgs e)
        {
            if (Stopped) return;
            Debug.Print("Logfile renamed.");
            Stop();
            OnRenamed(this, e);
        }

        private void _fsw_Error(object sender, ErrorEventArgs e)
        {
            Debug.Print("Error in FileWatcher.");
            //Stop();
        }

        private void _fsw_Changed(object sender, FileSystemEventArgs e)
        {
            if (Stopped) return;

            Debug.Print("File change event occurred.");

            if (e.ChangeType == WatcherChangeTypes.Changed)
            {
                if (LastWriteTimeChanged(false))
                {
                    OnChanged(sender, e);
                }
            }
            else
            {
                OnChanged(sender, e);
            }
        }

        private void OnChanged(object sender, FileSystemEventArgs e)
        {
            if (Changed != null) Changed(this, e);
        }

        private void OnRenamed(object sender, RenamedEventArgs e)
        {
            if (Renamed != null) Renamed(this, e);
        }

        private void PollTimer(object o)
        {
            if (LastWriteTimeChanged(true))
            {
                Debug.Print("File change detected by polling.");
                var args = new FileSystemEventArgs(WatcherChangeTypes.Changed, _fsw.Path, _fsw.Filter);
                OnChanged(this, args);
            }
        }

        private bool LastWriteTimeChanged(bool calledByPoller)
        {
            // On XP, the file's LastWriteTime changes automatically when the logger writes to the
            // file ONLY IF the size changes too.  If the file has wrapped (meaning the size stops changing),
            // the logger "manually" updates the LastWriteTime.  Thus, on XP, both properties change until
            // the file wraps, and then only the LastWriteTime changes.
            // On Vista, the LastWriteTime doesn't change automatically until the logger closes the file, period.
            // However, the size will change until the file wraps, then the logger starts "manually" setting
            // the LastWriteTime.  Thus, on Vista, only the size changes until the file wraps, then
            // only the LastWriteTime changes.

            var prevWriteTime = _lastWriteTime;
            var lastSize = _lastSize;

            lock (_lock)
            {
                try
                {
                    _fileInfo.Refresh();
                    _lastWriteTime = _fileInfo.LastWriteTime;
                    _lastSize = _fileInfo.Length;
                }
                catch (Exception ex)
                {
                    Stop();
                    return false;
                }

                if (prevWriteTime != _lastWriteTime || lastSize != _lastSize)
                {
                    ResetPollInterval();
                    return true;
                }
                else
                {
                    if (calledByPoller)
                    {
                        NextInterval();
                    }

                    return false;
                }
            }
        }
    }
}